I'm trying to make a Swift Chart where 24 AreaMarks an hour apart on X axis over a day display a vertical gradient.
The gradient is vertical and is essentially [Color.opacity(0.1),Colour,Color.opacity(0.1]
The idea here is where the upper and lower points of each AreaMark are the same or close to each other in the Y axis, the chart essentially displays a line, where they are far apart you get a nice fading vertical gradient.
However, it seems that the .alignsMarkStylesWithPlotArea modifier is always set for AreaMarks even if manually applying it false.
Investigating further, I've learnt that with AreaMarks in a series, Swift Charts seems to only listen to the first foreground style set in. I've created some sample code to demonstrate this.
struct DemoChartView: View {
var body: some View {
Chart {
AreaMark(x: .value("Time", Date().addingTimeInterval(0)), yStart: .value("1", 40), yEnd: .value("2", 60))
.foregroundStyle(LinearGradient(colors: [.pink, .teal], startPoint: .top, endPoint: .bottom))
.alignsMarkStylesWithPlotArea(false)
AreaMark(x: .value("Time", Date().addingTimeInterval(3600)), yStart: .value("1", 44), yEnd: .value("2", 58))
.foregroundStyle(LinearGradient(colors: [.orange, .yellow], startPoint: .top, endPoint: .bottom))
.alignsMarkStylesWithPlotArea(false)
AreaMark(x: .value("Time", Date().addingTimeInterval(03600*2)), yStart: .value("1", 50), yEnd: .value("2", 90))
.foregroundStyle(LinearGradient(colors: [.green, .blue], startPoint: .top, endPoint: .bottom))
.alignsMarkStylesWithPlotArea(false)
}
}
}
Which produces this:
So here, all the different .foregroundStyle LinearGradients are being ignored AND the .alignsMarkStylesWithPlotArea(false) is also ignored - the amount of pink on the first mark is different to the second and third 🤷♂️
Has anyone encountered this. Are AreaMarks the correct choice or are they just not setup to create this type of data display. Thanks
Swift Charts
RSS for tagVisualize data with highly customizable charts across all Apple platforms using the compositional syntax of SwifUI.
Posts under Swift Charts tag
54 Posts
Sort by:
Post
Replies
Boosts
Views
Activity
Hi, I'm trying to create a visualization using charts for vision pro. I want to create a line chart that connects pair of points on a donut chart. So i'm trying to draw the lines radially but it seems that the line chart always has only the bottom left corner of the view as origin. How can I tranform the origin to center of the view?
Is there a way to workaround this issue? Can I revert back to Beta 1?
Failed to build module 'Charts'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.7.41 clang-1600.0.24.1)', while this compiler is 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.9.11 clang-1600.0.26.2)'). Please select a toolchain which matches the SDK.
Using just basic rule
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 6)
Feature is working fine for iOS 17.x devices
Failed to build module 'Charts'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.7.41 clang-1600.0.24.1)', while this compiler is 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.9.11 clang-1600.0.26.2)'). Please select a toolchain which matches the SDK.
Any fix yet?
I can't figure out if I've found a VoiceOver problem with Swift Charts or if I'm doing something incorrectly.
I have a loop within a loop showing 2 sets of data in the same chart.
If I touch a month then VO correctly says there are two data series. But if I keep swiping down only data from the first series is read.
ChatGPT said try referencing the outer loop and sure enough that worked if it done in BOTH the label and value.
It sounds really awkward though. For example, "High 89 degrees F High October".
Below the "bad" chart only says something such as "92 degrees F October" when swiping down. The "good" chart will read the high and low temperature data.
VStack {
headerText("BAD")
Chart {
ForEach(processedMonthlyInput) { oneMonth in
ForEach(oneMonth.temperatures, id: \.month) { element in
LineMark(
x: .value("Month", element.month, unit: .month),
y: .value("Temperature", element.tempVal.converted(to: .fahrenheit).value)
)
.accessibilityLabel("\(element.month.formatted(.dateTime.month(.wide)))")
.accessibilityValue(Text("\(element.tempVal.converted(to: tempUnit).formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))"))
}
.symbol(by: .value("Type", oneMonth.theType))
.foregroundStyle(by: .value("Type", oneMonth.theType))
.interpolationMethod(.catmullRom)
}
}
.frame(maxHeight: paddingAmount)
.padding(.horizontal)
headerText("GOOD")
Chart {
ForEach(processedMonthlyInput) { oneMonth in
ForEach(oneMonth.temperatures, id: \.month) { element in
LineMark(
x: .value("Month", element.month, unit: .month),
y: .value("Temperature", element.tempVal.converted(to: .fahrenheit).value)
)
.accessibilityLabel("\(oneMonth.theType) \(element.month.formatted(.dateTime.month(.wide)))")
.accessibilityValue(Text("\(oneMonth.theType) \(element.tempVal.converted(to: tempUnit).formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))"))
}
.symbol(by: .value("Type", oneMonth.theType))
.foregroundStyle(by: .value("Type", oneMonth.theType))
.interpolationMethod(.catmullRom)
}
}
.frame(maxHeight: paddingAmount)
.padding(.horizontal)
}
Unable to compile app that imports Swift UI Charts SDK on Xcode Version 16.1 beta 2 (16B5014f) with error:
Failed to build module 'Charts'; this SDK is not supported by the compiler (the SDK is built with 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.7.41 clang-1600.0.24.1)', while this compiler is 'Apple Swift version 6.0 effective-5.10 (swiftlang-6.0.0.9.11 clang-1600.0.26.2)'). Please select a toolchain which matches the SDK.
FB15161667
Hello there!
I wanted to give a native scrolling mechanism for the Swift Charts Graph a try and experiment a bit if the scenario that we try to achieve might be possible, but it seems that the Swift Charts scrolling performance is very poor.
The graph was created as follows:
X-axis is created based on a date range,
Y-axis is created based on an integer values between moreless 0-320 value.
the graph is scrollable horizontally only (x-axis),
The time range (x-axis) for the scrolling content was set to one year from now date (so the user can scroll one year into the past as a minimum visible date (.chartXScale).
The X-axis shows 3 hours of data per screen width (.chartXVisibleDomain).
The data points for the graph are generated once when screen is about to appear so that the Charts engine can use it (no lazy loading implemented yet).
The line data points (LineMark views) consist of 2880 data points distributed every 5 minutes which simulates - two days of continuous data stream that we want to present. The rest of the graph displays no data at all.
The performance result:
The graph on the initial loading phase is frozen for about 10-15 seconds until the data appears on the graph.
Scrolling is very laggy - the CPU usage is 100% and is unacceptable for the end users.
If we show no data at all on the graph (so no LineMark views are created at all) - the result is similar - the empty graph scrolling is also very laggy.
Below I am sharing a test code:
@main
struct ChartsTestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
Spacer()
}
}
}
struct LineDataPoint: Identifiable, Equatable {
var id: Int
let date: Date
let value: Int
}
actor TestData {
func generate(startDate: Date) async -> [LineDataPoint] {
var values: [LineDataPoint] = []
for i in 0..<(1440 * 2) {
values.append(
LineDataPoint(
id: i,
date: startDate.addingTimeInterval(
TimeInterval(60 * 5 * i) // Every 5 minutes
),
value: Int.random(in: 1...100)
)
)
}
return values
}
}
struct ContentView: View {
var startDate: Date {
return endDate.addingTimeInterval(-3600*24*30*12) // one year into the past from now
}
let endDate = Date()
@State var dataPoints: [LineDataPoint] = []
var body: some View {
Chart {
ForEach(dataPoints) { item in
LineMark(
x: .value("Date", item.date),
y: .value("Value", item.value),
series: .value("Series", "Test")
)
}
}
.frame(height: 200)
.chartScrollableAxes(.horizontal)
.chartYAxis(.hidden)
.chartXScale(domain: startDate...endDate) // one year possibility to scroll back
.chartXVisibleDomain(length: 3600 * 3) // 3 hours visible on screen
.onAppear {
Task {
dataPoints = await TestData().generate(startDate: startDate)
}
}
}
}
I would be grateful for any insights or suggestions on how to improve it or if it's planned to be improved in the future.
Currently, I use UIKit CollectionView where we split the graph into smaller chunks of the graph and we present the SwiftUI Chart content in the cells, so we use the scrolling offered there. I wonder if it's possible to use native SwiftUI for such a scenario so that later on we could also implement some kind of lazy loading of the data as the user scrolls into the past.
Feedback FB14988865
Looks like in the latest version of Xcode 16.1 SwiftCharts crashes on launch. Below are the crashlogs.
dyld[88826]: Symbol not found: _$s6Charts12BuilderTupleVyxxQp_QPGAA8AxisMarkAARvzAaERzlMc
Referenced from: <01BB785A-84AF-3689-A614-DBFEB6A9733F> /Users/xxxx/Library/Developer/CoreSimulator/Devices/A6841249-F73B-45F2-AB68-96F94D75ACF7/data/Containers/Bundle/Application/E88B6681-E933-48AC-920A-150106F12A1F/xxxxx/xxxxx.debug.dylib
Expected in: <35624EEC-5BA2-3545-B05D-BABFE6661F1B> /Library/Developer/CoreSimulator/Volumes/iOS_22A5326g/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 18.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Charts.framework/Charts
It looks related to using AxisMarks to create custom axis.
SwiftUI Charts automatically groups accessibility elements on the graph (Double / Date for example) when there's a lot of data, which overrides the accessibilityLabel and value I set for each data point. This makes sense, but how do we modify the chart navigation accessibility readout when this grouping occurs?
Here's an example:
var body: some View {
let salesData: [(Date, Double)] = [
(Date().addingTimeInterval(-1 * 24 * 60 * 60), 1200),
(Date().addingTimeInterval(-2 * 24 * 60 * 60), 1500),
(Date().addingTimeInterval(-3 * 24 * 60 * 60), 1000),
(Date().addingTimeInterval(-4 * 24 * 60 * 60), 500),
(Date().addingTimeInterval(-5 * 24 * 60 * 60), 1500),
(Date().addingTimeInterval(-6 * 24 * 60 * 60), 1400),
(Date().addingTimeInterval(-7 * 24 * 60 * 60), 1300),
(Date().addingTimeInterval(-8 * 24 * 60 * 60), 1800),
(Date().addingTimeInterval(-9 * 24 * 60 * 60), 500),
(Date().addingTimeInterval(-10 * 24 * 60 * 60), 800),
(Date().addingTimeInterval(-11 * 24 * 60 * 60), 800),
(Date().addingTimeInterval(-12 * 24 * 60 * 60), 1000),
(Date().addingTimeInterval(-13 * 24 * 60 * 60), 1500),
(Date().addingTimeInterval(-14 * 24 * 60 * 60), 1500),
(Date().addingTimeInterval(-15 * 24 * 60 * 60), 900),
]
Chart {
ForEach(salesData, id: \.0) { date, sales in
LineMark(
x: .value("Foo", date),
y: .value("Bar", sales)
).accessibilityLabel("Foo: \(date.formatted(date: .abbreviated, time: .omitted)) Bar: \(sales.formatted(.currency(code: "USD")))")
}
}
.accessibilityElement(children: .contain)
}
}
I am wondering if there's a protocol, modifier.. or maybe something like UIAccessibilityContainerType.
On occasion, Swift Charts draws a large pink rectangle over the entire chart area as shown in the screen shot below.
I have never been able to reproduce this issue consistently, so I do not know if it is an issue with the data I input into the chart, or an Apple bug. It happens only rarely, for one redraw cycle, then the chart returns to the correct display.
Has anyone experienced this issue, and if so, what was the source of the problem and how did you resolve it?
I am plotting a SwiftUI chart from a Struct that looks like this:
struct BestWeights: Identifiable
{
let id = UUID()
let date: String
let maxWeight: Int
}
I am then creating an array of this Struct that I will use to plot the Chart:
private var bestWeights: [BestWeights] {
let unformattedMonth = DateFormatter().monthSymbols[month - 1]
let formattedMonth = String(unformattedMonth.prefix(3))
bestWeights.append(BestWeights(date: formattedMonth, maxWeight: bestWeightOfPeriod))
//decrementing down one month
selectedDate = Calendar.current.date(byAdding: .month, value: -1, to: selectedDate) ?? selectedDate
Then I am iterating through bestWeights and plotting them:
Chart {
ForEach(bestWeights) { bestWeight in
LineMark(x: .value("Date",bestWeight.date), y: .value("MaxWeight", bestWeight.maxWeight))
.symbol {
Circle()
.fill(.blue)
.frame(width: 7, height: 7)
}
}
}
The problem is that this produces 0 values on the Y axis that scrape the bottom of the LineMark, now I can fix this by not adding all of the bestWeights who's weight is 0 but then I don't get the full x axis I want which is 6 full months, it would show only the number of months as we have records and would jump from February to July etc.. is there any way to remove the 0 weights while keeping the X axis full of dates
I am showing weather data in a TabView where each tab will show the forecast for that day and then an hour-by-hour Swift Charts of the temperature, chance of precipitation, and then dew point for that day. The day and a text description of that forecast correctly displays for each tab. However, the chart will only show the correct data if you perform a tap gesture on the chart.
To reproduce this issue, I do the following:
Tap on a day in the "Upcoming Week", preferably a day in the middle.
Swipe a couple days to the left or right and use the tap gesture on one of the charts.
Oddly, other data on the tab is accurate, it is really just within the SwiftChart. I did try doing a horizontal ScrollView instead but I had the same issue. I also followed the demo for SwiftCharts from WWDC.
Below is code that shows the TabView:
struct UpcomingDaysView: View {
let forecastModel:WeatherModel
@State var targetPeriod:WeatherForecastDayPeriod
var body: some View {
TabView(selection:$targetPeriod) {
ForEach(forecastModel.dailyPeriods, id: \.self) {period in
DailyViewChartDetail(forecastModel: forecastModel, periodToShow: period)
.frame(alignment: .topLeading)
.tag(period)
}
}
.containerRelativeFrame([.vertical])
.tabViewStyle(.page)
.indexViewStyle(.page(backgroundDisplayMode: .always))
.padding(EdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 0))
}
}
}
And here is the DailyViewChartDetail:
struct DailyViewChartDetail: View {
var forecastModel:WeatherModel
var periodToShow:WeatherForecastDayPeriod
var body: some View {
VStack {
Text("\(DateUtility.getLongerDay(date: DateUtility.getDate(stringDate: periodToShow.dayForecast.startTime)))")
.font(.title2)
Text("\(periodToShow.dayForecast.detailedForecast)")
.font(.subheadline)
.padding(5)
HourlyViewChart(forecastModel: WeatherPeriodUtility.filterModel(forecastModel: forecastModel, targetPeriod: periodToShow))
.padding(5)
Spacer()
}
.frame(
alignment: .topLeading
)
}
}
And then some of the more relevant code in the HourlyViewChart
struct HourlyViewChart: View {
var forecastModel:WeatherModel
@State var range: (Date, Date)? = nil
@State var rawSelectedDateTemp: Date? = nil
@State var rawSelectedDatePrecipitation: Date? = nil
@State var rawSelectedDewPoint: Date? = nil
// ... more code
Below is an image that shows the issue:
Confused as to why the Chart flips with each user input. The console is also output unique id for each slice which was not my intention. Not sure if the unique .id is the culprit behind the flip.
selectedCount changed to: Optional(3)
Selected slice: Optional(App.EmojiUsage(id: 69090646-0D0A-4FE8-86EC-4103608DC3F7, emojiTab: App.emojiTab.sad, count: 1))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(1)
Selected slice: Optional(App.EmojiUsage(id: DE4A76D1-CC57-4FA0-A261-9AD1A6E28F95, emojiTab: App.emojiTab.happy, count: 2))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(3)
Selected slice: Optional(App.EmojiUsage(id: 5052F8EA-2582-4E72-A61D-01FCCDF3DB03, emojiTab: App.emojiTab.sad, count: 1))
Scheduling reset task to run in 2 seconds
Resetting selected slice and count
selectedCount changed to: Optional(0)
Selected slice: Optional(App.EmojiUsage(id: 5C1AB577-6CFC-4BA8-A9DF-30822EF79B91, emojiTab: App.emojiTab.happy, count: 2))
Scheduling reset task to run in 2 seconds
@Model
class AppModel {
var id: String
var journalEntry: String
var date: Date
var emojiTab: emojiTab
init(journalEntry: String, date: Date, emojiTab: emojiTab) {
self.id = UUID().uuidString
self.journalEntry = journalEntry
self.date = date
self.emojiTab = emojiTab
}
}
struct EmojiPrompt: Identifiable {
var id = UUID()
var icon: RiveViewModel
var emojitab: emojiTab
var title: String
}
enum emojiTab: String, Codable, Plottable {
case happy
case sad
case sleep
var primitivePlottable: Double {
switch self {
case .sleep:
return 0.0
case .happy:
return 1.0
case .sad:
return 2.0
}
}
}
var emojiPrompt = [
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "happyBtnSM",
artboardName: "happyBtn"
),
emojitab: .happy,
title: "Happy 1"
),
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "sadBtnSM",
artboardName: "sadBtn"
),
emojitab: .sad,
title: "Sad 2"
),
EmojiPrompt(
icon: RiveViewModel(
fileName: "app",
stateMachineName: "happyBtnSM",
artboardName: "happyBtn"
),
emojitab: .sleep,
title: "Sleep"
)
]
import SwiftUI
import SwiftData
import RiveRuntime
import Charts
struct SectorChartView: View {
@Environment(\.modelContext) private var context: ModelContext
@Binding var selectedEmojiUsage: EmojiUsage?
@State private var selectedCount: Int?
@Binding var selectedSlice: EmojiUsage?
@State private var resetTask: DispatchWorkItem? // State variable for the reset task
var emojiUsageData: [EmojiUsage]
var resetDelay: TimeInterval = 2.0 // Adjustable delay for reset
var body: some View {
ZStack {
Chart {
ForEach(emojiUsageData) { data in
SectorMark(
angle: .value("Count", data.count),
innerRadius: .ratio(0.70),
outerRadius: selectedSlice?.emojiTab == data.emojiTab ? .ratio(1.0) : .ratio(0.75),
angularInset: 1.5
)
.cornerRadius(4)
.foregroundStyle(by: .value("Emoji", data.emojiTab.rawValue.capitalized))
}
}
.chartAngleSelection(value: $selectedCount)
.chartBackground { chartProxy in
GeometryReader { geo in
let frame = geo[chartProxy.plotFrame!]
VStack {
if let selectedEmojiUsage = selectedEmojiUsage {
RiveViewModel(fileName: "app", stateMachineName: "\(selectedEmojiUsage.emojiTab.rawValue)BtnSM", artboardName: "\(selectedEmojiUsage.emojiTab.rawValue)Btn")
.view()
.frame(width: 120, height: 120)
.id(selectedEmojiUsage.emojiTab.rawValue) // Force re-render when the emojiTab changes
} else {
RiveViewModel(fileName: "app", stateMachineName: "sleepBtnSM", artboardName: "sleepBtn")
.view()
.frame(width: 120, height: 120)
.id("sleep") // Force re-render when default state
}
}
.position(x: frame.midX, y: frame.midY)
}
}
}
.onChange(of: selectedCount) { oldValue, newValue in
// Ensure reset task is only scheduled if there is a valid new value
guard newValue != nil else { return }
resetTask?.cancel() // Cancel any existing reset task
print("selectedCount changed to: \(String(describing: newValue))")
if let newValue = newValue {
withAnimation {
getSelectedSlice(value: newValue)
}
let task = DispatchWorkItem {
withAnimation(.easeIn) {
print("Resetting selected slice and count")
self.selectedSlice = nil
self.selectedCount = nil
self.selectedEmojiUsage = nil
}
}
resetTask = task
print("Scheduling reset task to run in 2 seconds")
DispatchQueue.main.asyncAfter(deadline: .now() + resetDelay, execute: task) // Schedule reset after specified delay
}
}
.frame(width: 250, height: 250)
}
private func getSelectedSlice(value: Int) {
var cumulativeTotal = 0
_ = emojiUsageData.first { emojiRange in
cumulativeTotal += emojiRange.count
if value <= cumulativeTotal {
selectedSlice = emojiRange
selectedEmojiUsage = emojiRange
print("Selected slice: \(String(describing: selectedSlice))")
return true
}
return false
}
}
}
var emojiUsageData: [EmojiUsage] {
let groupedEntries = Dictionary(grouping: entries, by: { $0.emojiTab })
return groupedEntries.map { (key, value) in
EmojiUsage(emojiTab: key, count: value.count)
}
}
struct EmojiUsage: Identifiable {
var id = UUID()
var emojiTab: emojiTab
var count: Int
}
(I'm using macOS 14.5 and Xcode 15.4)
I have a Swift Chart on macOS that needs to scroll horizontally. Simplified version:
Chart(dataPoints) { data in
LineMark(x: .value("X Axis", data.x),
y: .value("Y Axis", data.y))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 10)
The above code works fine, except that it does not show scroll bars.
On a Mac with no trackpad, this means there's no mechanism to scroll the chart.
On my MacBook Pro with a trackpad, I can scroll the chart with a 2-finger swipe gesture, but there are no transient scroll bars to show the relative size of the visible part of the chart.
How do I add visible scroll bars to the chart so that I can scroll on Macs with no trackpad?
Is there any approach or sample code available to use these APIs:
.chartScrollableAxes(.horizontal)
.chartScrollPosition(x: ...)
.chartScrollPosition(initialX: ...)
.chartScrollTargetBehavior(...)
.chartXVisibleDomain(length: ...)
Plus a gesture recognised or Pinch or Magnification to create a Swift Chart with an X axis that can be zoomed in or out with a pinch gesture? And when zoomed in at any level above 0, the chart can then be scrolled left to right along the X axis.
I've had success using .chartScrollableAxes with .chartXSelection in parallel, so would also like to keep the ability to select X values too.
Hi,
I'm currently wrestling with the .chartXScale(domain:) modifier in order to get my Chart to display correctly. The basics of the Chart look like this.
Chart(measurements, id: \.timestamp) { measurement in
if let total = measurement.production?.total {
BarMark(
x: .value(
"Timestamp",
measurement.timestamp,
unit: .weekOfYear,
calendar: .current
),
y: .value(
"Solar production",
total
)
)
}
}
As anyone familiar with Charts can see, I sort data into columns based on what week of the year the measurements belong to. Some of them can be null, and when they are, I still want space in the Chart where a BarMark would've been to be taken up, like week number 4 in this example chart (in which I've defaulted all measurements that are null in week 4 to 0, for demonstration purposes):
To achieve that, as I understand, I'm meant to use the .chartXScale(domain:) modifier, but when I apply the following modifier...
.chartXScale(domain: firstDayOfMonth...firstDayOfNextMonth)
... (where the domain is from the first day of the month to the first day of the next month), to the code above, I end up with this weird half step when the last week of measurements are all null:
For reference, here's how the domain dates are set in my minimum reproducible example:
firstDayOfMonth = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month], from: .now))!
firstDayOfNextMonth = Calendar.current.date(byAdding: .month, value: 1, to: firstDayOfMonth)!
Am I misusing this modifier somehow, or is this a bug? Would love some help figuring this out, thanks!
I have a SwiftUI page that I want to simplify by showing basic information by default, and putting the additional info behind a "Details" DisclosureGroup for advanced users.
I started by laying out all the components and breaking things into individual Views. These all are laid out and look fine.
Then I took several of them and added them inside a DisclosureGroupView.
But all of a sudden, the views inside started getting crunched together and the contents of the DisclosureGroup got clipped about 2/3 of the way down the page. The problem I'm trying to solve is how to show everything inside the DIsclosureGroup.
The top-level View looks like this:
VStack {
FirstItemView()
SecondView()
DetailView() // <- Shows disclosure arrow
}
Where DetailView is:
struct DetailView: View {
@State var isExpanded = true
var body: some View {
GeometryReader { geometry in
DisclosureGroup("Details", isExpanded: $isExpanded) {
ThirdRowView()
Spacer()
FourthRowView()
VStack {
FifthRowWithChartView()
CaptionLabelView(label: "Third", iconName: "chart.bar.xaxis")
}
}
}
}
}
The FifthRowWithChartView is half-clipped. One thing that might contribute is that there is a Chart view at the bottom of this page.
I've tried setting the width and height of the DisclosureGroup based on the height returned by the GeometryReader, but that didn't do anything.
This is all on iOS 17.6, testing on an iPhone 15ProMax. Any tips or tricks are most appreciated.
Using Charts in SwiftUI to create a horizontal bar chart, if the text of the legend is sufficiently long, the text overflows outside of the view rather than wrapping or moving to the next line. (can see problem when running code on on iPhone)
Is this a bug or am I doing something incorrectly?
I can use .clipped() to ensure the legend doesn't overflow. But that doesn't fully solve the problem because the text is then just cut off.
import Charts
import SwiftUI
struct ChartData: Identifiable {
let id: Int
let title: String
let count: Double
let color: Color
}
struct ContentView: View {
private let data = [
ChartData(id: 1, title: "Item 1", count: 4, color: .yellow),
ChartData(id: 2, title: "Item 2 With a Long Title and then some more", count: 6, color: .blue),
ChartData(id: 3, title: "Item 3 With a Long Title", count: 12, color: .green)
]
private let chartHeight: CGFloat = 40
private let chartCornerRadius: CGFloat = 5
var body: some View {
VStack {
Chart(data) { item in
BarMark(
x: .value("Count", item.count),
stacking: .normalized
)
.foregroundStyle(by: .value("Title", item.title))
}
.chartXAxis(.hidden)
.chartYAxis(.hidden)
.chartLegend(.visible)
.chartPlotStyle { chartContent in
chartContent
.frame(height: chartHeight)
}
.chartForegroundStyleScale(range: data.map { $0.color })
}
.padding()
}
}
#Preview {
ContentView()
}
I'm building a Swift Chart that displays locations in a horizontal timeline. The chart is scrollable.
When the chart is zoomed in, I want to show an annotation for every 6 hours.
Unfortunately, when axisMarks are set to .stride(by: .hour, count: 6), the annotations do not appear for the first several months in the timeline. I tried setting .stride(by: .minute, count: 360), but the result is the same.
Is this a Swift Charts bug, or am I doing something wrong?
A reproducible example is below. To reproduce:
Run the code below
See that annotations are missing at the leading edge of the chart. They only show up from a certain point on the chart's domain.
Tested on various iPhone and iPad simulators and physical devices, the issue appears everywhere.
P.S. I am aware that the example code below is not performant and that the annotations overlap when the chart is zoomed out. I have workarounds for that, but it's beyond the scope of my question and the minimum reproducible example.
struct ChartAnnotationsBug: View {
/// Sample data
let data = SampleData.samples
let startDate = SampleData.samples.first?.startDate ?? Date()
let endDate = Date()
/// Scroll position of the chart, expressed as Date along the x-axis.
@State var chartPosition: Date = SampleData.samples.first?.startDate ?? Date()
/// Sets the granularity of the shown view.
@State var visibleDomain: VisibleDomain = .month
var body: some View {
Chart(data, id: \.id) { element in
BarMark(xStart: .value("Start", element.startDate),
xEnd: .value("End", element.endDate),
yStart: 0,
yEnd: 50)
.foregroundStyle(by: .value("Type", element.type.rawValue))
.clipShape(.rect(cornerRadius: 8, style: .continuous))
}
.chartScrollableAxes(.horizontal) // enable scroll
.chartScrollPosition(x: $chartPosition) // track scroll offset
.chartXVisibleDomain(length: visibleDomain.seconds)
.chartXScale(domain: startDate...endDate)
.chartXAxis {
AxisMarks(values: .stride(by: .hour, count: 6)) { value in
if let date = value.as(Date.self) {
let hour = Calendar.current.component(.hour, from: date)
if hour == 0 { // midnight
AxisValueLabel(collisionResolution: .truncate) {
VStack(alignment: .leading) {
Text(date, format: .dateTime.hour().minute())
Text(date, format: .dateTime.weekday().month().day())
.bold()
}
}
AxisTick(stroke: .init(lineWidth: 1))
} else if [6, 12, 18].contains(hour) { // period
AxisValueLabel(collisionResolution: .truncate) {
Text(date, format: .dateTime.hour().minute())
}
AxisTick(length: .label)
}
}
}
}
.frame(height: 100)
.padding(.bottom, 40) // for overlay picker
.overlay {
Picker("", selection: $visibleDomain.animation()) {
ForEach(VisibleDomain.allCases) { variant in
Text(variant.label)
.tag(variant)
}
}
.pickerStyle(.segmented)
.frame(width: 240)
.padding(.trailing)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
} //: overlay
} //: body
} //: struct
// MARK: - Preview
#Preview {
ChartAnnotationsBug()
}
// MARK: - Data
enum SampleDataType: String, CaseIterable {
case city, wood, field
var label: String {
switch self {
case .city:
"City"
case .wood:
"Wood"
case .field:
"Field"
}
}
}
enum VisibleDomain: Identifiable, CaseIterable {
case day
case week
case month
var id: Int {
self.seconds
}
var seconds: Int {
switch self {
case .day:
3600 * 24 * 2
case .week:
3600 * 24 * 10
case .month:
3600 * 24 * 40
}
}
var label: String {
switch self {
case .day:
"Days"
case .week:
"Weeks"
case .month:
"Months"
}
}
}
struct SampleData: Identifiable {
let startDate: Date
let endDate: Date
let name: String
let type: SampleDataType
var id: String { name }
static let samples: [SampleData] = [
.init(startDate: Date.from(year: 2024, month: 3, day: 1, hour: 23, minute: 59),
endDate: Date.from(year: 2024, month: 3, day: 10),
name: "New York",
type: .city),
.init(startDate: Date.from(year: 2024, month: 3, day: 10, hour: 6),
endDate: Date.from(year: 2024, month: 3, day: 20),
name: "London",
type: .city),
.init(startDate: Date.from(year: 2024, month: 3, day: 20),
endDate: Date.from(year: 2024, month: 4, day: 10),
name: "Backcountry ABC",
type: .field),
.init(startDate: Date.from(year: 2024, month: 4, day: 10),
endDate: Date.from(year: 2024, month: 4, day: 20),
name: "Field DEF",
type: .field),
.init(startDate: Date.from(year: 2024, month: 4, day: 20),
endDate: Date.from(year: 2024, month: 5, day: 10),
name: "Wood 123",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 5, day: 10),
endDate: Date.from(year: 2024, month: 5, day: 20),
name: "Paris",
type: .city),
.init(startDate: Date.from(year: 2024, month: 5, day: 20),
endDate: Date.from(year: 2024, month: 6, day: 5),
name: "Field GHI",
type: .field),
.init(startDate: Date.from(year: 2024, month: 6, day: 5),
endDate: Date.from(year: 2024, month: 6, day: 10),
name: "Wood 456",
type: .wood),
.init(startDate: Date.from(year: 2024, month: 6, day: 10),
endDate: Date(),
name: "Field JKL",
type: .field)
]
}
extension Date {
/**
Constructs a Date from a given year (Int). Use like `Date.from(year: 2020)`.
*/
static func from(year: Int? = nil, month: Int? = nil, day: Int? = nil, hour: Int? = nil, minute: Int? = nil) -> Date {
let components = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
guard let date = Calendar.current.date(from: components) else {
print(#function, "Failed to construct date. Returning current date.")
return Date()
}
return date
}
}